package com.google.gwt.gfx.client;

import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.SimpleEventBus;
import com.google.gwt.gfx.client.GFXPanel.Tool;
import com.google.gwt.user.client.DOM;

/**
 * A type of {@link Tool} provides mouse dragging support of a {@link GFXPanel}
 * .
 */
public class DragTool<B extends GFXPanel<?, ?, ?>> implements Tool<B>,
		HasDragHandlers, MouseDownHandler, MouseMoveHandler, MouseOutHandler,
		MouseUpHandler {

	private B buddyRef;

	private boolean dragging = false;

	private EventBus eventBus = new SimpleEventBus();

	private HandlerRegistration mouseDown;

	private HandlerRegistration mouseMove;

	private HandlerRegistration mouseOut;

	private HandlerRegistration mouseUp;

	@Override
	public HandlerRegistration addDragHandler(final DragEvent.Handler handler) {
		return eventBus.addHandler(DragEvent.TYPE, handler);
	}

	public void bind(final B buddy) {
		buddyRef = buddy;
	}

	public B buddy() {
		return buddyRef;
	}

	@Override
	public void disable() {
		mouseDown.removeHandler();
		mouseMove.removeHandler();
		mouseOut.removeHandler();
		mouseUp.removeHandler();
	}

	@Override
	public void enable() {
		mouseDown = buddy().addMouseDownHandler(this);
		mouseMove = buddy().addMouseMoveHandler(this);
		mouseOut = buddy().addMouseOutHandler(this);
		mouseUp = buddy().addMouseUpHandler(this);
	}

	/**
	 * Fires a {@link DragEvent}.
	 * 
	 * @param <E>
	 *            a type of {@link MouseEvent} the drag event wraps on
	 * @param type
	 *            type of dragging
	 * @param event
	 *            the drag event wraps on
	 */
	public <E extends MouseEvent<?>> void fireDragEvent(DragEvent.Type type,
			E event) {
		fireEvent(new DragEvent<E>(type, event.getClientX()
				- buddy().getAbsoluteLeft(), event.getClientY()
				- buddy().getAbsoluteTop(), event));
	}

	@Override
	public void fireEvent(GwtEvent<?> event) {
		eventBus.fireEvent(event);
	}

	/**
	 * Gets true if the viewer is dragging.
	 * 
	 * @return true if the viewer is dragging
	 */
	public boolean isDragging() {
		return dragging;
	}

	private <E extends MouseEvent<?>> void onDragBegin(E event) {
		fireDragEvent(DragEvent.Type.Begin, event);
		DOM.setCapture(buddy().getElement());
		dragging = true;
	}

	private <E extends MouseEvent<?>> void onDragContinue(E event) {
		fireEvent(new DragEvent<E>(DragEvent.Type.Continue, event.getClientX()
				- buddy().getAbsoluteLeft(), event.getClientY()
				- buddy().getAbsoluteTop(), event));
	}

	private <E extends MouseEvent<?>> void onDragEnd(E event) {
		fireEvent(new DragEvent<E>(DragEvent.Type.End, event.getClientX()
				- buddy().getAbsoluteLeft(), event.getClientY()
				- buddy().getAbsoluteTop(), event));
		DOM.releaseCapture(buddy().getElement());
		dragging = false;
	}

	@Override
	public void onMouseDown(MouseDownEvent event) {
		if (isDragging()) {
			return;
		}
		onDragBegin(event);
	}

	@Override
	public void onMouseMove(MouseMoveEvent event) {
		if (!isDragging()) {
			return;
		}
		onDragContinue(event);
	}

	@Override
	public void onMouseOut(MouseOutEvent event) {
		if (!isDragging()) {
			return;
		}
		onDragEnd(event);
	}

	@Override
	public void onMouseUp(MouseUpEvent event) {
		if (!isDragging()) {
			return;
		}
		onDragEnd(event);
	}

}
